跳到主要内容

SpringBoot 整合 AOP

回顾 Spring AOP 术语

  • 通知(Advice)包含了需要用于多个应用对象的横切行为,通俗一点说就是定义了 “什么时候” 和 “做什么”。
  • 连接点(Join Point)是程序执行过程中能够应用通知的所有点。
  • 切点(Point)是定义了在“什么地方”进行切入,哪些连接点会得到通知。显然,切点一定是连接点。
  • 切面(Aspect)是通知和切点的结合。通知和切点共同定义了切面的全部内容——是什么,何时,何地完成功能。
  • 引入(Introduction)允许我们向现有的类中添加新方法或者属性。
  • 织入(Weaving)是把切面应用到目标对象并创建新的代理对象的过程,分为编译期织入、类加载期织入和运行期织入。

引入依赖

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

编写用于拦截的 Bean

直接定义一个 Controller,代码如下:

@RestController
public class AopController {

@RequestMapping("/hello")
public String sayHello(){
System.out.println("hello");
return "hello";
}
}

定义切面

Spring 采用 @AspectJ 注解对 POJO 进行标注,该注解表明该类不仅仅是一个 POJO,还是一个切面。切面是切点和通知的结合,那么定义一个切面就需要编写切点和通知。

在代码中,只需要添加 @Aspect 注解即可。

定义切点

切点是通过 @Pointcut 注解和切点表达式定义的。

@Pointcut 注解可以在一个切面内定义可重用的切点。

由于 Spring 切面粒度最小是达到方法级别,而 execution 表达式可以用于明确指定方法返回类型,类名,方法名和参数名等与方法相关的部件,并且实际中,大部分需要使用 AOP 的业务场景也只需要达到方法级别即可,因而 execution 表达式的使用是最为广泛的。

如图是 execution 表达式的语法:

execution表示在方法执行的时候触发。以 * 开头,表明方法返回值类型为任意类型。然后是全限定的类名和方法名,* 可以表示任意类和任意方法。

对于方法参数列表,可以使用 .. 表示参数为任意类型。如果需要多个表达式,可以使用 “&&”、“||” 和 “!” 完成与、或、非的操作。

定义通知

通知有五种类型,分别是:

  • 前置通知(@Before):在目标方法调用之前调用通知
  • 后置通知(@After):在目标方法完成之后调用通知
  • 环绕通知(@Around):在被通知的方法调用之前和调用之后执行自定义的方法
  • 返回通知(@AfterReturning):在目标方法成功执行之后调用通知
  • 异常通知(@AfterThrowing):在目标方法抛出异常之后调用通知

示例代码

代码中定义了三种类型的通知,使用 @Before 注解标识前置通知,打印 “beforeAdvice...”,使用 @After 注解标识后置通知,打印 “AfterAdvice...”,使用 @Around 注解标识环绕通知,在方法执行前和执行之后分别打印 “before” 和 “after”。

这样一个切面就定义好了,代码如下:

@Aspect
@Component
public class AopAdvice {

@Pointcut("execution (* com.example.aop.controller.*.*(..))")
public void test() {

}

@Before("test()")
public void beforeAdvice() {
System.out.println("beforeAdvice...");
}

@After("test()")
public void afterAdvice() {
System.out.println("afterAdvice...");
}

@Around("test()")
public void aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) {
System.out.println("before");
try {
// 调用这个方法,使原方法继续执行
proceedingJoinPoint.proceed();
} catch (Throwable t) {
t.printStackTrace();
}
System.out.println("after");
}

}

这样在 controller 包下的任意方法执行时都会执行切面的代码

AspectJ 切入点指示符

切入点指示符用来指示切入点表达式目的,,在Spring AOP中目前只有执行方法这一个连接点,Spring AOP 支持的 AspectJ 切入点指示符如下:

  • execution:用于匹配方法执行的连接点;
  • within:用于匹配指定类型内的方法执行;
  • this:用于匹配当前 AOP 代理对象类型的执行方法;注意是 AOP 代理对象的类型匹配,这样就可能包括引入接口也类型匹配;
  • target:用于匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配;
  • args:用于匹配当前执行的方法传入的参数为指定类型的执行方法;
  • @within:用于匹配所以持有指定注解类型内的方法;
  • @target:用于匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解;
  • @args:用于匹配当前执行的方法传入的参数持有指定注解的执行;
  • @annotation:用于匹配当前执行方法持有指定注解的方法;
  • bean:Spring AOP扩展的,AspectJ没有对于指示符,用于匹配特定名称的Bean对象的执行方法;
  • reference pointcut:表示引用其他命名切入点,只有@ApectJ风格支持,Schema风格不支持。

示例:

@Around("@annotation(com.alsritter.requestlimit.annotation.RequestRateLimit)")
public Object requestRateLimit(ProceedingJoinPoint point) throws Throwable {
// do something...
return point.proceed();
}

AspectJ 切入点支持的切入点指示符还有: call、get、set、preinitialization、staticinitialization、initialization、handler、adviceexecution、withincode、cflow、cflowbelow、if、@this、@withincode;

但Spring AOP目前不支持这些指示符,使用这些指示符将抛出 IllegalArgumentException 异常。这些指示符 Spring AOP 可能会在以后进行扩展。

within 匹配类型

within 是用来指定类型的,指定类型中的所有方法将被拦截。

// AService下面所有外部调用方法,都会拦截。备注:只能是AService的方法,子类不会拦截的
@Pointcut("within(com.fsx.run.service.AService)")
public void pointCut() {
}

所以此处需要注意:上面写的是 AService 接口,是达不到拦截效果的,只能写实现类:

//此处只能写实现类
@Pointcut("within(com.fsx.run.service.impl.AServiceImpl)")
public void pointCut() {
}

匹配包以及子包内的所有类:

@Pointcut("within(com.fsx.run.service..*)")
public void pointCut() {
}

this 匹配代理对象

Spring Aop 是基于代理的,this 就表示代理对象。this 类型的 Pointcut 表达式的语法是 this(type),当生成的代理对象可以转换为 type 指定的类型时则表示匹配。

基于 JDK 接口的代理和基于 CGLIB 的代理生成的代理对象是不一样的。(注意和上面 within 的区别)

// 这样子,就可以拦截到AService所有的子类的所有外部调用方法
@Pointcut("this(com.fsx.run.service.AService*)")
public void pointCut() {
}

target 匹配代理对象

Spring Aop是基于代理的,target 则表示被代理的目标对象。当被代理的目标对象可以被转换为指定的类型时则表示匹配。

注意:和上面不一样,这里是 target,因此如果要切入,只能写实现类了

@Pointcut("target(com.fsx.run.service.impl.AServiceImpl)")
public void pointCut() {
}

@target 匹配当被代理的目标对象对应的类型及其父类型上拥有指定的注解时。

//能够切入类上(非方法上)标准了MyAnno注解的所有外部调用方法
@Pointcut("@target(com.fsx.run.anno.MyAnno)")
public void pointCut() {
}

args 匹配参数

args用来匹配方法参数的。

1、args() 匹配任何不带参数的方法。 2、args(java.lang.String) 匹配任何只带一个参数,而且这个参数的类型是String的方法。 3、args(…) 带任意参数的方法。 4、args(java.lang.String,…) 匹配带任意个参数,但是第一个参数的类型是 String 的方法。 5、args(…,java.lang.String) 匹配带任意个参数,但是最后一个参数的类型是 String 的方法。

@Pointcut("args()")
public void pointCut() {
}

这个匹配的范围非常广,所以一般和别的表达式结合起来使用

@annotation 匹配注解

@annotation 用于匹配方法上拥有指定注解的情况。

// 可以匹配所有方法上标有此注解的方法
@Pointcut("@annotation(com.fsx.run.anno.MyAnno)")
public void pointCut() {
}

我们还可以这么写,非常方便的获取到方法上面的注解

@Before("@annotation(myAnno)")
public void doBefore(JoinPoint joinPoint, MyAnno myAnno) {
System.out.println(myAnno); //@com.fsx.run.anno.MyAnno()
System.out.println("AOP Before Advice...");
}

也可以使用反射的方式取得注解:

@Around("@annotation(com.alsritter.requestlimit.annotation.RequestRateLimit)")
public Object requestRateLimit(ProceedingJoinPoint point) throws Throwable {
// ...
// Gets method
final Method method = ((MethodSignature) point.getSignature()).getMethod();
// Gets annotation
RequestRateLimit requestRateLimit = method.getAnnotation(RequestRateLimit.class);
// Gets annotation params
RateLimitEnum limitEnum = requestRateLimit.limit();
TimeUnit timeUnit = requestRateLimit.timeUnit();
// ...
return point.proceed();
}

获取方法上的参数

@Aspect
@Component
@Slf4j
public class DemoAop {

/**
* 环绕通知
* @param proceedingJoinPoint
* @return
* @throws Throwable
*/
@Around(value = "execution(* com.example.aopdemo..*(..)))")
public Object demoAop(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {

log.debug("执行前:");

Map<String, Object> params = getNameAndValue(proceedingJoinPoint);
for (Map.Entry<String, Object> entry : params.entrySet()) {
System.out.println("name: " + entry.getKey() + " value: " + entry.getValue());
}

Object object = proceedingJoinPoint.proceed(); //执行连接点方法,object:方法返回值

log.debug("执行后:");

return object;
}
/**
* 获取参数Map集合
* @param joinPoint
* @return
*/
Map<String, Object> getNameAndValue(ProceedingJoinPoint joinPoint) {
Map<String, Object> param = new HashMap<>();
Object[] paramValues = joinPoint.getArgs();
String[] paramNames = ((CodeSignature)joinPoint.getSignature()).getParameterNames();
for (int i = 0; i < paramNames.length; i++) {
param.put(paramNames[i], paramValues[i]);
}
return param;
}